(function(window) {
	
	//Paul Irish's requestAnimationFrame polyfill (slightly modyfied)
	//http://paulirish.com/2011/requestanimationframe-for-smart-animating/
	if (typeof window.requestAnimationFrame !== "function") {
		window.requestAnimationFrame = (function() {
			return window.webkitRequestAnimationFrame
				|| window.mozRequestAnimationFrame
				|| window.oRequestAnimationFrame
				|| window.msRequestAnimationFrame
				|| function(callback){
					window.setTimeout(callback, 1000 / 60);
				};
		}());
	}
	
	var 
		
		idRegex = /[a-zA-Z_][a-zA-Z0-9_]*/,
		
		//constructor
		sCanvas = function(selector, ready, error) {
			//self-reference
			var loaded,
				sc = this;
			
			//check selector
			if (typeof selector !== "string" && !selector.nodeType) {
				throw new TypeError("Selector must be string or HTMLElement.");
			}
			
			//save selector
			this.selector = selector;
			
			this.initFields();
			
			//bind primary ready handler
			this.ready(this.init);
			
			//bind passed handlers
			if (typeof ready === "function") {
				this.ready(ready);
			}
			
			if (typeof error === "function") {
				this.error(error);
			}
			
			//if DOM was already loaded, fire ready immediatelly
			if (document.readyState === "complete") {
				this.ready();
			} else {
				loaded = function() {
					window.removeEventListener("load", loaded, false);
					document.removeEventListener("DOMContentLoaded", loaded, false);
					sc.ready();
				};
				
				//better way to detect DOM load
				document.addEventListener("DOMContentLoaded", loaded, false);

				//load will always work
				window.addEventListener("load", loaded, false);
			}
		};
	
	//returns actuall time as a number
	sCanvas.now = function() {
		return (new Date).getTime();
	};
	
	//checks if passed argument is valid ID, returns true if it is valid
	sCanvas.isValidId = function(id) {
		return id.match(idRegex) !== null;
	};
	
	//checks if passed object is group
	sCanvas.isGroup = function(obj) {
		return (obj instanceof CanvasGroup);
	};
		
	sCanvas.fn = sCanvas.prototype = {
		
		//reassigns constructor
//		constructor: sCanvas,
		
		//stores original canvas selector passed to constructor
		selector: null,
		
		//stores elemement wrapping canvas
		wrapper: null,
		
		//stores front canvas
		front: null,
		
		//stores back canvas
		back: null,
		
		//stores context of front canvas
		frontCx: null,
				
		//stores context of back canvas
		backCx: null,
				
		//stores event handlers
		handlers: null,
		
		//remembers if canvas is already ready
		isReady: false,
		
		//remembers if canvas is already initialized with error
		isError: false,
		
		//stores all canvas objects as map (id: object)
		objects: null,
		
		//stores currently animated objects
		animated: null,
		
		//count of currently animated objects
		animatedCount: 0,
		
		//true if canvas is currently performing an animation
		isAnimating: false,
		
		//stores bottom canvas object
		bottom: null,
		
		//stores top canvas object
		top: null,
		
		//stores if canvas is autorepainting
		autorepainting: true,
		
		//stores default attributes values of newly created objects
		defaultAttrs: null,
		
		//stores object with registered events
		events: null,
		
		//stores last coordinates during mousemove
		lastMovePoint: null,
		
		//stores canvas position in element
		pos: null,
		
		initFields: function() {
			this.handlers = {
				ready: [ ],
				error: [ ]
			};
			
			this.objects = { };
			
			this.animated = [ ];
			
			this.defaultAttrs = {
				x: 0,
				y: 0,
				rotation: 0,
				id: null,
				group: [ ],
				visible: true,
				stroke: null,
				fill: "#000000"
			};
			
			this.events = {
				mousemove: [ ],
				mouseenter: [ ],
				mouseleave: [ ],
				mouseup: [ ],
				mousedown: [ ],
				click: [ ],
				dblclick: [ ]
			};
			
			this.pos = {
				top: 0,
				left: 0
			};
		},
		
		//initializes sCanvas
		//is invoked as first ready handler
		init: function(evt) {
			var err = null;
			
			//get wrapper element and save it
			if (typeof this.selector === "string") {
				this.wrapper = document.getElementById(this.selector);
			} else if (this.selector.nodeType) {
				this.wrapper = this.selector;
			}
			
			//get canvas - do not raise error when canvas was not 
			try {
				this.front = this.wrapper.getElementsByTagName("canvas")[0];
			} catch (err) {}
			
			//check if canvas was found
			if (!!this.front) {
				//get context and save it
				this.frontCx = this.front.getContext("2d");

				//check if context was created properly
				if (!!this.frontCx) {
					this.pos = this.position();
					this.wrap();
				} else {
					this.frontCx = null;
					err = new Error("This browser does not support canvas.");
				}
			} else {
				err = new ReferenceError("Canvas element was not found.");
			}
			
			//if error occured, stop propagating ready event and trigger error event
			if (!!err) {
				evt.stopImmediatePropagation();
				this.trigger("error", [err], true);
			} else {
				//sCanvas was initialized properly
				//so there is no need to store error handlers anymore
				this.handlers.error = [ ];
			}
		},
		
		//creates html structure, adds necessary styles
		wrap: function() {
			var wrapperStyles = window.getComputedStyle(this.wrapper);
			
			//make wrapper offset parent
			if (wrapperStyles.getPropertyValue("position") === "static") {
				this.wrapper.style.position = "relative";
			}
			
			//add canvas absolute position styles
			this.front.style.position = "absolute";
			this.front.style.top = "0px";
			this.front.style.left = "0px";
			this.front.style.zIndex = "2";
			
			//clone original canvas
			this.back = this.front.cloneNode();
			this.back.style.zIndex = "1";
			this.backCx = this.back.getContext("2d");
			this.wrapper.appendChild(this.back);
		},
		
		//returns canvas object with passed id
		get: function(id) {
			if (this.objects[id] !== undefined) {
				return this.objects[id];
			} else {
				return this.null();
			}
		},
		
		//adds object with id
		addIdObject: function(obj, id) {
			this.objects[id] = obj;
		},
		
		//changes objects id
		changeIdObject: function(oldId, newId) {
			this.objects[newId] = this.objects[oldId];
			delete this.objects[oldId];
		},
		
		//remove object with id
		removeIdObject: function(id) {
			delete this.objects[id];
		},
		
		//counts canvas position in element
		position: function() {
			var left = 0,
				top = 0,
				obj = this.front;
			
			while (obj !== null) {
				left += obj.offsetLeft;
				top += obj.offsetTop;
				
				obj = obj.offsetParent;
			}
			
			return {
				left: left,
				top: top
			};
		},
		
		//registers ready event handler or triggers ready event
		ready: function(fn) {
			if (fn === undefined) {
				//triggering event
				this.trigger("ready", true);
				
				//remember that ready event has been already triggered
				this.isReady = true;
			} else {
				//registering handler
				//if ready already occured, execute it immeditelly
				if (this.isReady) {
					this.bind("ready", fn).trigger("ready", true);
				} else {
					this.bind("ready", fn);
				}
			}
			return this;
		},
		
		//registers error event handler or triggers error event
		error: function(fn) {
			if (fn === undefined) {
				//triggering event
				this.trigger("error", true);
				
				//remember that error event has been already triggered
				this.isError = false;
			} else {
				//registering handler
				//if error already occured, execute it immeditelly
				if (this.isError) {
					this.bind("error", fn).trigger("error", true);
				} else {
					this.bind("error", fn);
				}
			}
			return this;
		},
		
		//binds event handler
		bind: function(event, fn) {
			if (typeof fn !== "function") {
				throw new TypeError("Event handler was not passed.");
			}
			
			if (this.handlers[event] === undefined) {
				throw new ReferenceError("Passed event is not supported.");
			}
			
			this.handlers[event].push(fn);
			
			return this;
		},
		
		//triggers event
		//remove is internal argument, should not be used externally
		trigger: function(event, data, remove) {
			var args, i;
			
			//check if passed event type is supported
			if (this.handlers[event] === undefined) {
				throw new ReferenceError("Passed event is not supported.");
			}
			
			//unify arguments
			if (typeof data === "boolean") {
				remove = data;
				data = undefined;
			}
			
			//TODO: replace with browser data
			args = [new EventObject({ })];
			if (Array.isArray(data)) {
				for (i = 0; i < data.length; i++) {
					args.push(data[i]);
				}
			}
			
			//execute all binded handlers
			for (i = 0; i < this.handlers[event].length; i++) {
				this.handlers[event][i].apply(this, args);
				
				//if executing of rest of the handlers was stopped
				if (args[0].isImmediatePropagationStopped()) {
					break;
				}
			}
			
			//if remove is set to true, handlers are removed
			if (remove === true) {
				this.handlers[event] = [ ];
			}
			
			return this;
		},
		
		//returns group object with passed id
		//if there is no group with this id, new one is created
		group: function(id) {
			var obj = this.get(id);
			
			if (obj.isNull()) {
				if (sCanvas.isValidId(id)) {
					obj = new CanvasGroup(this, id);
					this.addIdObject(obj, id);
				} else {
					throw new Error("Passed ID is not valid.");
				}
			} else if (!sCanvas.isGroup(obj)) {
				throw new Error("Object identified by passed ID is not a group.");
			}
			
			return obj;
		},
		
		//returns empty element
		//usefull on edge situations, where is expected returning object that
		//was not found and throwing error is not desired
		null: function() {
			return new CanvasNull(this);
		},
		
		//creates rect object
//		rect: function(x, y, width, height) {
		rect: function(attrs) {
			var obj = new CanvasRect(this);
			
			obj.attr(attrs);
			
			this.register(obj);
			this.autorepaint();
			return obj;
		},
		
		//creates rect object
		circ: function(attrs) {
			var obj = new CanvasCirc(this);
			
			obj.attr(attrs);
			
			this.register(obj);
			this.autorepaint();
			return obj;
		},
		
		//creates image object
		image: function(source, attrs) {
			try {
				var obj = new CanvasImage(this, source);

				obj.attr(attrs);

				this.register(obj);
//				this.autorepaint();
				return obj;
			} catch (err) {
				throw(err);
			}
		},
		
		//creates text object
		text: function(text, attrs) {
			try {
				var obj = new CanvasText(this, text);

				obj.attr(attrs);

				this.register(obj);
				this.autorepaint();
				return obj;
			} catch (err) {
				throw err;
			}
		},
		
		//performs all common tasks while creating new object
		register: function(obj) {
			if (this.top !== null) {
				this.top.over(obj);
			} else {
				//if top is null, botom is also null (i.e. there is no object yet)
				this.bottom = obj;
			}
			this.top = obj;
		},
		
		//performs repaint if autorepainting is on
		autorepaint: function() {
			if (this.isAutorepainting()) {
				this.repaint();
			}
		},
		
		//returns true if autorepainting is on
		isAutorepainting: function() {
			return this.autorepainting;
		},
		
		//repaints canvas
		repaint: function() {
			var obj;
			
			//TODO: think of some optimization to canvas clearing
			this.frontCx.clearRect(0, 0, this.width(), this.height());
			
			obj = this.bottom;
			while (obj !== null) {
				obj.draw(this.frontCx);
				obj = obj.overObj;
			}
			
			return this;
		},
		
		//gets/sets canvas width
		width: function(w) {
			if (w === undefined) {
				//getter
				return this.front.width;
			} else {
				//setter
				//TODO: think of performing repaint
				this.front.width = w;
				this.back.width = w;
				return this;
			}
		},
		
		//gets/sets canvas width
		height: function(h) {
			if (h === undefined) {
				//getter
				return this.front.height;
			} else {
				//setter
				//TODO: think of performing repaint
				this.front.height = h;
				this.back.height = h;
				return this;
			}
		},
		
		//registers animated object
		addAnimatedObject: function(obj) {
			this.animated.push(obj);
			this.animatedCount++;
			//TODO: handle adding already added object
			//if animation is not running, run it
			if (!this.isAnimating) {
				this.isAnimating = true;
				this.runAnimation();
			}
		},
		
		//unregisters animated object
		removeAnimatedObject: function(obj) {
			delete this.animated[this.animated.indexOf(obj)];
			this.animatedCount--;
			//TODO: handle removing object which was not added
			//if there is no animated object, stop animating
			if (this.animatedCount === 0) {
				this.isAnimating = false;
				this.animated = [ ];
			}
		},
		
		//performs animation frame by frame
		runAnimation: function() {
			var i,
				sc = this,
				time = sCanvas.now();
			
			//prevents requesting next frame, if there is no animated object
			//also prevents needless repainting - since we are requesting
			//frame and then we count if it is needed at all
			//so there is always one "blank" requested frame at the end
			//of animation when nothing is done
			//TODO: think if this can be fixed
			if (this.isAnimating) {
				window.requestAnimationFrame(function() {
					sc.runAnimation();
				});
			
				//update all animated objects attributes
				for (i in this.animated) {
					this.animated[i].stepAnimation(time);
				}
				this.repaint();
			}
		},
		
		//registers object with event
		addEventObject: function(obj, event) {
			var sc = this;
			
			this.events[event].push(obj);
			
			if (event === "click" || event === "dblclick" || event === "mouseup" || event === "mousedown") {
				if (this.events[event].length < 2) {
					this.front["on" + event] = function(e) {
						sc.triggerClickEvent(e, event);
					};
				}
			} else if (event === "mousemove" || event === "mouseleave" || event === "mouseenter") {
				if (this.events["mousemove"].length + this.events["mouseleave"].length + this.events["mouseenter"].length < 2) {
					this.front["onmousemove"] = function(e) {
						sc.triggerMoveEvent(e);
					};
				}
			}
		},
		
		//unregisters object with event 
		removeEventObject: function(obj, event) {
			
		},
		
		//if click was performed over object, triggers its event
		triggerClickEvent: function(e, event) {
			var i, ex, ey, obj;
			
			ex = e.pageX - this.pos.left;
			ey = e.pageY - this.pos.top;
			for (i in this.events[event]) {
				var obj = this.events[event][i];
				if (this.isPointInPath(obj, ex, ey)) {
					obj.trigger(event);
				}
			}
		},
		
		//if move was performed over object, triggers its event
		triggerMoveEvent: function(e) {
			var i, ex, ey, obj;
			//TODO: optimize this piece of shit
			ex = e.pageX - this.pos.left;
			ey = e.pageY - this.pos.top;
			
			if (this.events["mouseenter"].length > 0) {
				for (i in this.events["mouseenter"]) {
					var obj = this.events["mouseenter"][i];
					if (this.isPointInPath(obj, ex, ey) && (this.lastMovePoint === null || !this.isPointInPath(obj, this.lastMovePoint.x, this.lastMovePoint.y))) {
						obj.trigger("mouseenter");
					}
				}
			}
			
			if (this.events["mousemove"].length > 0) {
				for (i in this.events["mousemove"]) {
					var obj = this.events["mousemove"][i];
					if (this.isPointInPath(obj, ex, ey)) {
						obj.trigger("mousemove");
					}
				}
			}
			
			if (this.events["mouseleave"].length > 0) {
				for (i in this.events["mouseleave"]) {
					var obj = this.events["mouseleave"][i];
					if (!this.isPointInPath(obj, ex, ey) && (this.lastMovePoint !== null && this.isPointInPath(obj, this.lastMovePoint.x, this.lastMovePoint.y))) {
						obj.trigger("mouseleave");
					}
				}
			}
			
			this.lastMovePoint = { x: ex, y: ey	};
		},
		
		//checks if specified point is in object
		isPointInPath: function(obj, x, y) {
			var result = false;
			if (typeof this.frontCx.isPointInPath === "function") {
				obj.createFillPath(this.frontCx);
				result = this.frontCx.isPointInPath(x, y);
			} else {
				
			}
			return result;
		}
		
	};
	
	//extends first object with one or more others
	sCanvas.extend = sCanvas.fn.extend = function() {
		var result = arguments[0],
			i = 1,
			key;
		
		//if only one argument is passed, sCanvas will be extended
		if (arguments.length === 1) {
			result = this;
			i = 0;
		} else if (typeof result !== "object") {
			result = { };
		}
		
		for (; i < arguments.length; i++) {
			for (key in arguments[i]) {
				result[key] = arguments[i][key];
			}
		}
		
		return result;
	};
	
	
	//<editor-fold defaultstate="collapsed" desc="### EventObject ###">
	
	//object containing event data, it allows to manipulate event behaviour
	var EventObject = function(data) {
		return this;
	};
	
	//if true prevents executing next event handler
	EventObject.prototype.stopImmediateProp = false;
	
	//prevents next event handlers from being executed
	EventObject.prototype.stopImmediatePropagation = function() {
		this.stopImmediateProp = true;
	};
	
	//returns true if executing next event handlers was stopped
	EventObject.prototype.isImmediatePropagationStopped = function() {
		return this.stopImmediateProp;
	};
	
	//</editor-fold>
	
	
	//<editor-fold defaultstate="collapsed" desc="### CanvasObject ###">
	
	var 
		//parent of all canvas objects
		CanvasObject = function() {
			return this;
		};
	
	CanvasObject.prototype = {
		
		//stores sCanvas reference
		sc: null,
		
		//stores object attributes
		attrs: null,
		
		//stores actual absolute object origin position in pixels
		//also stores info if origin is absolute or relative
		origin: null,
		
		//stores supported attributes
		supportedAttrs: null,
		
		//stores supported attributes
		animatableAttrs: null,
		
		//stores handler binded to attribute change
		attrHandlers: null,
		
		//stores reference to the over object
		overObj: null,
		
		//stores reference to the under object
		underObj: null,

		//stores animation queue
		animations: null,
		
		//stores event handlers
		handlers: null,
		
		//initializes object
		init: function(sc) {
			this.sc = sc;
			
			this.handlers = {
				mousemove: [ ],
				mouseenter: [ ],
				mouseleave: [ ],
				mouseup: [ ],
				mousedown: [ ],
				click: [ ],
				dblclick: [ ]
			};
			
			this.supportedAttrs = (
				"x y rotation xOrigin yOrigin visible"
				+ " fill stroke id group"
			).split(" ");
			
			this.animatableAttrs = (
				"x y rotation"
			).split(" ");
			
			this.attrHandlers = {
				id: this.setId,
				group: this.setGroup,
				xOrigin: this.setOrigin,
				yOrigin: this.setOrigin
			};
			
			this.origin = {
				x: 0,
				y: 0
			};
			
			this.animations = new AnimationQueue();
		},
		
		//returns true if object is null object
		isNull: function() {
			return (this instanceof CanvasNull);
		},
		
		//makes object visible
		show: function() {
			if (this.isNull()) {
				return this;
			}
			
			this.attr("visible", true);
			
			return this;
		},

		//makes object invisible
		hide: function() {
			if (this.isNull()) {
				return this;
			}
			
			this.attr("visible", false);
			
			return this;
		},
		
		//returns true if object is visible
		isVisible: function() {
			return !!this.attrs.visible;
		},
		
		//creates path for drawing filled object
		createFillPath: function(cx) {
			throw new Error("Method CanvasObject.createFillPath() should be overriden!");
		},
		
		//creates path for drawing object stroke
		createStrokePath: function(cx) {
			throw new Error("Method CanvasObject.createStrokePath() should be overriden!");
		},
		
		//returns the most top point of object
//		top: function() {
//			throw new Error("Method CanvasObject.top() should be overriden!");
//		},
		
		//returns the most top point of object
//		bottom: function() {
//			throw new Error("Method CanvasObject.bottom() should be overriden!");
//		},
		
		//returns the most left point of object
//		left: function() {
//			throw new Error("Method CanvasObject.left() should be overriden!");
//		},
		
		//returns the most left point of object
//		right: function() {
//			throw new Error("Method CanvasObject.right() should be overriden!");
//		},
		
		//returns the width of object
//		width: function() {
//			throw new Error("Method CanvasObject.width() should be overriden!");
//		},
//		
//		//returns the height of object
//		height: function() {
//			throw new Error("Method CanvasObject.height() should be overriden!");
//		},
		
		//returns the width of object
		baseWidth: function() {
			throw new Error("Method CanvasObject.baseWidth() should be overriden!");
		},
		
		//returns the height of object
		baseHeight: function() {
			throw new Error("Method CanvasObject.baseHeight() should be overriden!");
		},
		
		//returns center point as relative coordinates from base top left of object
		center: function() {
			return {
				x: this.baseWidth() / 2,
				y: this.baseHeight() / 2
			};
		},
		
		//draws object on canvas
		draw: function(cx) {
			if (this.attrs.stroke !== null) {
				this.createStrokePath(cx);
				cx.strokeStyle = this.attrs.stroke;
				cx.stroke();
			}
			
			if (this.attrs.fill !== null) {
				this.createFillPath(cx);
				cx.fillStyle = this.attrs.fill;
				cx.fill();
			}
		},
		
		//gets/sets over object
		over: function(obj) {
			if (this.isNull()) {
				return this;
			}
			
			if (obj === undefined) {
				//getter
				return this.overObj;
			} else {
				//setter
				
				//update old neighbours of passed object
				if (obj.overObj !== null) {
					obj.overObj.underObj = obj.underObj;
				}
				if (obj.underObj !== null) {
					obj.underObj.overObj = obj.overObj;
				}
				
				//update passed object
				obj.overObj = this.overObj;
				obj.underObj = this;
				
				////update new neighbours of passed object
				if (this.overObj !== null) {
					this.overObj.underObj = obj;
				}
				this.overObj = obj;
				
				return this;
			}
		},

		//gets/sets under object
		under: function(obj) {
			if (this.isNull()) {
				return this;
			}
			
			if (obj === undefined) {
				//getter
				return this.underObj;
			} else {
				//setter
				
				//update old neighbours of passed object
				if (obj.overObj !== null) {
					obj.overObj.underObj = obj.underObj;
				}
				if (obj.underObj !== null) {
					obj.underObj.overObj = obj.overObj;
				}
				
				//update passed object
				obj.underObj = this.underObj;
				obj.overObj = this;
				
				////update new neighbours of passed object
				if (this.underObj !== null) {
					this.underObj.overObj = obj;
				}
				this.underObj = obj;
				
				return this;
			}
		},
		
		//adds supported attribute for object
		addSupportedAttr: function(attr) {
			var i;
			if (typeof attr === "string") {
				this.supportedAttrs.push(attr);
			} else {
				for (i = 0; i < attr.length; i++) {
					this.supportedAttrs.push(attr[i]);
				}
			}
		},
		
		//adds animatable attribute for object
		addAnimatableAttr: function(attr) {
			var i;
			if (typeof attr === "string") {
				this.animatableAttrs.push(attr);
			} else {
				for (i = 0; i < attr.length; i++) {
					this.animatableAttrs.push(attr[i]);
				}
			}
		},
		
		//initializes attrs with supported attributes and sets them to default value
		initAttrs: function() {
			var attr, i,
				defaults = this.sc.defaultAttrs;
		
			this.attrs = { };
			for (i = 0; i < this.supportedAttrs.length; i++) {
				attr = this.supportedAttrs[i];
				if (defaults[attr] !== undefined) {
					this.attrs[attr] = defaults[attr];
				} else {
					this.attrs[attr] = null;
				}
			}
			//TODO: handle this nicely
			this.attrs.group = [ ];
		},
		
		//gets/sets object attribute(s)
		attr: function(name, value) {
			if (this.isNull()) {
				return this;
			}
			
			var attr, temp;
			
			//handling one attribute
			if (typeof name === "string") {
				//check if attribute is supported
				if (!this.attrExists(name)) {
					throw new Error("Attribute " + name + " is not supported.");
				}
				
				if (value === undefined) {
					//getting value
					return this.attrs[name];
				} else {
					//setting value
					this.setAttr(name, value);
					this.sc.autorepaint();
					return this;
				}
			}
			
			//setting multiple attributes
			//TODO: check if name is map
			
			//ensure setting x/yOrigin after setting x/y
//			if (name.xOrigin !== undefined) {
//				temp = name.xOrigin;
//				delete name.xOrigin;
//				name.xOrigin = temp;
//			}
//			if (name.yOrigin !== undefined) {
//				temp = name.yOrigin;
//				delete name.yOrigin;
//				name.yOrigin = temp;
//			}
			
			for (attr in name) {
				if (!this.attrExists(attr)) {
					throw new Error("Attribute " + attr + " is not supported.");
				} else {
					this.setAttr(attr, name[attr]);
				}
			}
			this.sc.autorepaint();
			return this;
		},
		
		//checks if attribute is supported
		attrExists: function(name) {
			return this.attrs.hasOwnProperty(name);
		},
		
		//sets attribute value
		setAttr: function(name, value) {
			//if is set handler for attribut change invoke it
			//otherwise just set attribute value
			if (this.attrHandlers[name] !== undefined) {
				this.attrHandlers[name].apply(this, [value, this.attrs[name], name]);
			} else {
				this.attrs[name] = value;
			}
		},
		
		//invoked when object id is set
		setId: function(id, oldId) {
			if (id === "" || id === null) { //if id is removed
				if (oldId !== null) {
					this.sc.removeIdObject(oldId);
					this.attrs.id = null;
				}
			} else if (sCanvas.isValidId(id)) { //if id is valid
				if (this.sc.get(id).isNull()) { //if id is not already used
					if (oldId !== null) { //if object already has id
						this.sc.changeIdObject(oldId, id);
					} else {
						this.sc.addIdObject(this, id);
					}
					this.attrs.id = id;
				} else {
					throw new Error("Passed ID is already used.");
				}
			} else {
				throw new Error("Passed ID is not valid.");
			}
		},
		
		//invoked when object group is set
		setGroup: function(group, oldGroup) {
			var i, groups;
			
			//remove all current groups
			if (oldGroup !== null) {
				for (i = 0; i < oldGroup.length; i++) {
					this.removeGroup(oldGroup[i]);
				}
			}
			this.attrs.group = [ ];
			
			if (group === "" || group === null) {
				;
			} else if (typeof group === "string") {
				groups = group.split(" ");
				
				for (i = 0; i < groups.length; i++) {
					if (!groups[i].match(/^\s*$/)) { //if group is not empty string
						this.addGroup(groups[i]);
					}
				}
			} else {
				//TODO: check if passed object is group
				group.add(this);
			}
		},
		
		//invoked when object origin attribute is set
		setOrigin: function(value, old, origin) {
			var axis;
			if (origin === "xOrigin") {
				axis = "x";
			} else {
				axis = "y";
			}
			
			if (typeof value === "number") {
				//this.origin[axis] = this.attrs[axis] - old + value;
				this.origin[axis] = value;
			} else if (typeof value === "string") {
				if (value.charAt(0) === "=") {
					//this.origin[axis] = parseFloat(value.slice(1), 10);
					this.origin[axis] = -this.attrs[axis] + old + parseFloat(value.slice(1), 10);
//				} else if (value.charAt(value.length - 1) === "%") {
//					this.origin[axis] = edge + Math.round(parseFloat(value, 10) * size / 100);
				} else {
					//this.origin[axis] = this.attrs[axis] - old + parseFloat(value, 10);
					this.origin[axis] = parseFloat(value, 10);
				}
			} // else do nothing
			
			//recount x/y attribute so object stays on position
			this.attrs[axis] = this.attrs[axis] - old + this.origin[axis];
			
			this.attrs[origin] = value;
		},
		
		//adds object to group
		addGroup: function(id) {
			this.sc.group(id).add(this);
			
			return this;
		},
		
		//removes object from group
		removeGroup: function(id) {
			this.sc.group(id).remove(this);
			
			return this;
		},
		
		//returns true if object is in group
		inGroup: function(id) {
			return this.sc.group(id).has(this);
		},
		
		//adds group to object attribute
		//internal
		group: function(id) {
			if (this.attrs.group === null) {
				this.attrs.group = [ id ];
			} else {
				this.attrs.group.push(id);
			}
		},
		
		//removes group from object attribute
		//internal
		ungroup: function(id) {
			var i = this.attrs.group.indexOf(id);
			if (i !== -1) {
				this.attrs.group.splice(i, 1);
			}
		},
		
		//registers object animation
		animate: function(attr, duration, easing, complete) {
			if (this.isNull()) {
				return this;
			}
			
			var options, anim;
			
			//unify arguments and make options map
			if (duration === undefined) {
				options = { };
			} else if (typeof duration === "number" || typeof duration === "string") {
				options = {
					duration: duration
				};
				
				if (typeof easing === "string") {
					options.easing = easing;
				} else if (typeof easing === "function") {
					options.complete = easing;
				}
				
				if (typeof complete === "function") {
					options.complete = complete;
				}
			} else if (typeof duration === "function") {
				options = {
					complete: duration
				};
			} else {
				options = duration;
			}
			
			this.addAnimation(new Animation(this, attr, options));
			
			return this;
		},
		
		//adds function to animation queue
		queue: function(fn) {
			if (this.isNull()) {
				return this;
			}
			
			if (typeof fn !== "function") {
				throw new TypeError("Function was not passed.");
			}
			
			this.addAnimation(new Animation(this, { }, {
				duration: 0,
				complete: fn
			}));
			
			return this;
		},
		
		//delay execution of animations in animation queue
		delay: function(duration) {
			if (this.isNull()) {
				return this;
			}
			
			this.addAnimation(new Animation(this, { }, {
				duration: duration
			}));
			
			return this;
		},
		
		//stop currently running animation
		stop: function(clear, jump) {
			if (this.isNull()) {
				return this;
			}
			
			if (this.animations.isEmpty()) {
				return this;
			}
			
			var anim = this.animations.peek(),
				time = sCanvas.now();
			
			if (!!jump) {
				sCanvas.extend(this.attrs, anim.finish());
				if (anim.complete !== null) {
					anim.complete.apply(this, [ ]);
				}
				this.sc.repaint();
			}
			
			if (!!clear) {
				this.clearQueue();
			}
			
			if (anim.isRepeated()) {
				console.log(anim.startAttrs["rotation"]);
				anim.repeat(true);
				console.log(anim.startAttrs["rotation"]);
				this.stepAnimation(time);
			} else {
				this.animations.remove();

				if (this.animations.isEmpty()) {
					this.sc.removeAnimatedObject(this);
				} else {
					this.animations.peek().start(time);
					this.stepAnimation(time);
				}
			}
			
			return this;
		},
		
		//removes all animations that have not started
		clearQueue: function() {
			if (this.isNull()) {
				return this;
			}
			
			if (!this.animations.isEmpty()) {
				this.animations.peek().repeats = 1;
				this.animations.removeWaiting();
			}
			
			return this;
		},
		
		//immediatelly finishes whole animation queue
		finish: function() {
			if (this.isNull()) {
				return this;
			}
			
			var anim;
			
			if (!this.animations.isEmpty()) {
				do {
					anim = this.animations.peek();
					anim.computeAttrs();
					sCanvas.extend(this.attrs, anim.finish());
					if (anim.complete !== null) {
						anim.complete.apply(this, [ ]);
					}
					this.animations.remove();
				} while (!this.animations.isEmpty());
				this.sc.repaint();
				this.sc.removeAnimatedObject(this);
			}
			
			return this;
		},
		
		//pauses running animation
		pause: function() {
			if (this.isNull()) {
				return this;
			}
			
			if (!this.animations.isEmpty()) {
				this.animations.peek().pause();
			}
			
			return this;
		},
		
		//reruns paused animation
		play: function() {
			if (this.isNull()) {
				return this;
			}
			
			if (!this.animations.isEmpty()) {
				this.animations.peek().play();
			}
			
			return this;
		},
		
		//returns true if animation is paused
		isPaused: function() {
			if (!this.animations.isEmpty()) {
				return this.animations.peek().isPaused();
			} else {
				return false;
			}
		},
		
		//changes object attributes during animation
		stepAnimation: function(time) {
			var anim = this.animations.peek();
			sCanvas.extend(this.attrs, anim.next(time));
			
			//if animation finished, fire next, or if there is no one, tell it to canvas
			if (anim.isFinished()) {
				if (anim.complete !== null) {
					anim.complete.apply(this, [ ]);
				}
				
				if (anim.isRepeated()) {
					anim.repeat();
					this.stepAnimation(time);
				} else {
					this.animations.remove();

					if (this.animations.isEmpty()) {
						this.sc.removeAnimatedObject(this);
					} else {
						this.animations.peek().start(anim.ending);
						this.stepAnimation(time);
					}
				}
			}
		},

		//pushes animation to animation queue
		addAnimation: function(anim) {
			this.animations.add(anim);
			
			//if animation queue was empty before adding new animation - fire it
			if (this.animations.length === 1) {
				anim.start();
				this.sc.addAnimatedObject(this);
			}
		},
		
		//registers click event handler or triggers click event
		click: function(fn) {
			if (this.isNull()) {
				return this;
			}
			
			if (fn === undefined) {
				//triggering event
				this.trigger("click", true);
			} else {
				//registering handler
				this.bind("click", fn);
			}
			return this;
		},
		
		//binds event handler
		bind: function(event, fn) {
			if (this.isNull()) {
				return this;
			}
			
			if (typeof fn !== "function") {
				throw new TypeError("Event handler was not passed.");
			}
			
			if (this.handlers[event] === undefined) {
				throw new ReferenceError("Passed event is not supported.");
			}
			
			if (this.handlers[event].length === 0) {
				this.sc.addEventObject(this, event);
			}
			
			this.handlers[event].push(fn);
			
			return this;
		},
		
		//triggers event
		trigger: function(event, data) {
			if (this.isNull()) {
				return this;
			}
			
			var args, i;
			
			//check if passed event type is supported
			if (this.handlers[event] === undefined) {
				throw new ReferenceError("Passed event is not supported.");
			}
			
			//TODO: replace with browser data
			args = [new EventObject({ })];
			if (Array.isArray(data)) {
				for (i = 0; i < data.length; i++) {
					args.push(data[i]);
				}
			}
			
			//execute all binded handlers
			for (i = 0; i < this.handlers[event].length; i++) {
				this.handlers[event][i].apply(this, args);
				
				//if executing of rest of the handlers was stopped
				if (args[0].isImmediatePropagationStopped()) {
					break;
				}
			}
			
			return this;
		}
				
	};
	
	//</editor-fold>
	
	//<editor-fold defaultstate="collapsed" desc="### CanvasObjectNull ###">
	
	var CanvasNull = function(sc) {
		this.init(sc);
		this.initAttrs();
		return this;
	};
	
	CanvasNull.prototype = new CanvasObject();
	
	CanvasNull.prototype.createFillPath = function(cx) {};
	
	CanvasNull.prototype.createStrokePath = function(cx) {};
	
//	CanvasNull.prototype.top = function() {};
//	
//	CanvasNull.prototype.left = function() {};
//	
//	CanvasNull.prototype.width = function() {};
//	
//	CanvasNull.prototype.height = function() {};
	
	//</editor-fold>
	
	//<editor-fold defaultstate="collapsed" desc="### CanvasObjectRect ###">
	
	var CanvasRect = function(sc) {
		this.init(sc);
		this.addSupportedAttr(["width", "height"]);
		this.addAnimatableAttr(["width", "height"]);
		this.initAttrs();
		return this;
	};
	
	CanvasRect.prototype = new CanvasObject();
	
	CanvasRect.prototype.createFillPath = function(cx) {
		var center = this.center();
		cx.save();
		cx.translate(this.attrs.x, this.attrs.y);
		cx.rotate(this.attrs.rotation);
		cx.beginPath();
		cx.moveTo(-center.x - this.origin.x, -center.y - this.origin.y);
		cx.lineTo(-center.x - this.origin.x + this.attrs.width, -center.y - this.origin.y);
		cx.lineTo(-center.x - this.origin.x + this.attrs.width, -center.y - this.origin.y + this.attrs.height);
		cx.lineTo(-center.x - this.origin.x, -center.y - this.origin.y + this.attrs.height);
		cx.lineTo(-center.x - this.origin.x, -center.y - this.origin.y);
		cx.closePath();
		cx.restore();
	};
	
	CanvasRect.prototype.createStrokePath = function(cx) {
		this.createFillPath(cx);
	};
	
//	CanvasRect.prototype.top = function() {
//		return this.attrs.y - this.center().y - this.origin.y;
//	};
//	
//	CanvasRect.prototype.bottom = function() {
//		return this.attrs.y - this.center().y - this.origin.y + this.attrs.height;
//	};
//	
//	CanvasRect.prototype.left = function() {
//		return this.attrs.x - this.center().x - this.origin.x;
//	};
//	
//	CanvasRect.prototype.right = function() {
//		return this.attrs.x - this.center().x - this.origin.x + this.attrs.width;
//	};
	
	CanvasRect.prototype.baseWidth = function() {
		return this.attrs.width;
	};
	
	CanvasRect.prototype.baseHeight = function() {
		return this.attrs.height;
	};
	
	//</editor-fold>
	
	//<editor-fold defaultstate="collapsed" desc="### CanvasObjectCirc ###">
	
	var CanvasCirc = function(sc) {
		this.init(sc);
		this.addSupportedAttr("radius");
		this.addAnimatableAttr("radius");
		this.initAttrs();
		return this;
	};
	
	CanvasCirc.prototype = new CanvasObject();
	
	CanvasCirc.prototype.createFillPath = function(cx) {
		cx.beginPath();
		cx.arc(this.attrs.x, this.attrs.y, this.attrs.radius, 0, 2 * Math.PI, false);
		cx.closePath();
	};
	
	CanvasCirc.prototype.createStrokePath = function(cx) {
		this.createFillPath(cx);
	};
	
//	CanvasCirc.prototype.top = function() {
//		return 0;
//	};
//	
//	CanvasCirc.prototype.left = function() {
//		return 0;
//	};
	
	CanvasCirc.prototype.baseWidth = function() {
		return 2 * this.attrs.radius;
	};
	
	CanvasCirc.prototype.baseHeight = function() {
		return 2 * this.attrs.radius;
	};
	
	//</editor-fold>
		
	//<editor-fold defaultstate="collapsed" desc="### CanvasObjectImage ###">
	
	var CanvasImage = function(sc, source) {
		var that = this;
		this.init(sc);
		this.addSupportedAttr(["width", "height", "wait"]);
		this.addAnimatableAttr(["width", "height"]);
		this.initAttrs();
		
		this.source = source;
		
		this.img = new Image();
		
		if (source instanceof Image) {
			this.img.src = source.src;
		} else if (typeof source === "string") {
			this.img.src = source;
		} else {
			throw new TypeError("Bad image source.");
		}
		
		this.img.onload = function() {
			that.loaded = true;
			
			if (that.attrs.width === null) { //if width was not set to resize image
				that.attrs.width = this.width;
			}
			if (that.attrs.height === null) { //if height was not set to resize image
				that.attrs.height = this.height;
			}
			
			that.sc.autorepaint();
		};
		
		return this;
	};
	
	CanvasImage.prototype = new CanvasObject();
	
	CanvasImage.prototype.createFillPath = function(cx) {};
	
	CanvasImage.prototype.createStrokePath = function(cx) {};
	
	CanvasImage.prototype.draw = function(cx) {
		if (this.loaded) {
			cx.save();
			cx.translate(this.attrs.x, this.attrs.y);
			cx.rotate(this.attrs.rotation);
			cx.drawImage(this.img, 0, 0, this.attrs.width, this.attrs.height);
			cx.restore();
		}
	};
	
//	CanvasImage.prototype.top = function() {
//		return 0;
//	};
//	
//	CanvasImage.prototype.left = function() {
//		return 0;
//	};
	
	CanvasImage.prototype.baseWidth = function() {
		return this.attrs.width;
	};
	
	CanvasImage.prototype.baseHeight = function() {
		return this.attrs.height;
	};
	
	//stores passed image source
	CanvasImage.prototype.source = null;
	
	//stores HTMLImage object created from source
	CanvasImage.prototype.img = null;
	
	//returns true if image is loaded
	CanvasImage.prototype.loaded = false;
	
	//</editor-fold>
	
	//<editor-fold defaultstate="collapsed" desc="### CanvasObjectText ###">
	
	var CanvasText = function(sc, text) {
		this.init(sc);
		this.addSupportedAttr(["fontFamily", "fontSize", "fontWeight", "fontStyle", "align", "baseline", "direction"]);
		this.initAttrs();
		
		try {
			this.val(text);
		} catch (err) {
			throw err;
		}
		
		return this;
	};
	
	CanvasText.prototype = new CanvasObject();
	
	CanvasText.prototype.createFillPath = function(cx) { };
	
	CanvasText.prototype.createStrokePath = function(cx) { };
	
	CanvasText.prototype.draw = function(cx) {
		cx.font = (this.attrs.fontStyle || "") + " "
				+ (this.attrs.fontWeight || "") + " "
				+ (this.attrs.fontSize || "") + " "
				+ (this.attrs.fontFamily || "");
		cx.textAlign = this.attrs.align;
		cx.textBaseline = this.attrs.baseline;
		cx.direction = this.attrs.direction;
		
		if (this.attrs.stroke !== null) {
			cx.strokeStyle = this.attrs.stroke;
			cx.strokeText(this.textContent, this.attrs.x, this.attrs.y);
		}

		if (this.attrs.fill !== null) {
			cx.fillStyle = this.attrs.fill;
			cx.fillText(this.textContent, this.attrs.x, this.attrs.y);
		}
	};
	
//	CanvasText.prototype.top = function() {
//		return 0;
//	};
//	
//	CanvasText.prototype.left = function() {
//		return 0;
//	};
	
	CanvasText.prototype.baseWidth = function() {
		return 10;
	};
	
	CanvasText.prototype.baseHeight = function() {
		return 10;
	};
	
	//stores text
	CanvasText.prototype.textContent = null;
	
	//gets/sets text
	CanvasText.prototype.val = function(text) {
		if (text === undefined) {
			return this.textContent;
		} else {
			if (typeof text === "string" || typeof text === "number") {
				this.textContent = text;
				this.sc.autorepaint();
			} else {
				throw new TypeError("Passed argument must be string or number.");
			}
		}
	};
	
	//</editor-fold>
	
	
	
	//<editor-fold defaultstate="collapsed" desc="### CanvasGroup ###">
	
	var 
		//parent of all canvas objects
		CanvasGroup = function(sc, id) {
			this.sc = sc;
			this.objects = [ ];
			this.id = id;
			return this;
		};
	
	CanvasGroup.prototype = {
		
		//stores sCanvas reference
		sc: null,
		
		//stores count of grouped objects
		length: 0,
		
		//stores references to grouped objects
		objects: null,
		
		//stores group id
		id: null,
		
		//adds object to the group
		add: function(obj) {
			//only non-null and non-group object is added and only if it is not already in group
			if (!obj.isNull() && !sCanvas.isGroup(obj) && !this.has(obj)) {
				this.objects.push(obj);
				this.length = this.objects.length;
				obj.group(this.id);
			}
			return this;
		},
		
		//if called with no argument of false, group is destroyed, but objects live on
		//if called with true, group and all grouped onjects are destroyed
		//if called with object, this object is removed from group
		//if object is wanted to be removed, call remove on this object directly
		//if called with integer, object at this position is removed
		//if object at position is wanted to be removed, call obj.get(i).remove()
		remove: function(obj) {
			if (obj === undefined) {
				obj = false;
			}
			
			var i;
			
			if (typeof obj === "boolean") {
				if (obj) {
					for (i in this.objects) {
						this.objects[i].remove();
					}
				} else {
					for (i in this.objects) {
						this.objects[i].ungroup(this.id);
					}
				}
				this.sc.removeIdObject(this.id);
				
				return this.sc.null();
			} else if (typeof obj === "number") {
				i = Math.floor(obj);
			} else {
				i = this.objects.indexOf(obj);
			}
			
			if (i >= 0 && i < this.objects.length) {
				this.objects[i].ungroup(this.id);
				this.objects.splice(i, 1);
			}
			
			this.length = this.objects.length;

			return this;
		},
		
		//returns true if group contains passed object
		has: function(obj) {
			return this.objects.indexOf(obj) !== -1;
		},
		
		//returns object at i position
		get: function(i) {
			if (this.objects[i] !== undefined) {
				return this.objects[i];
			} else {
				return this.sc.null();
			}
		},
		
		//calls passed function on all grouped objects
		each: function(fn) {
			var i, ret;
			
			if (typeof fn !== "function") {
				throw new TypeError("Function was not passed.");
			}
			
			for (i in this.objects) {
				ret = fn.apply(this.objects[i], [ Number(i) ]);
				if (ret === false) {
					break;
				}
			}
		},
		
		//returns true if group is empty
		isEmpty: function() {
			return this.length === 0;
		},
		
		//returns false as group is never considered null
		//TODO: think about returning false if group is empty, true otherwise
		isNull: function() {
			return false;
		},
		
		//makes object visible
		show: function() {
			if (this.isNull()) {
				return this;
			}
			
			var i;
			for (i in this.objects) {
				this.objects[i].show();
			}
			
			return this;
		},

		//makes object invisible
		hide: function() {
			if (this.isNull()) {
				return this;
			}
			
			var i;
			for (i in this.objects) {
				this.objects[i].hide();
			}
			
			return this;
		},
		
		//returns true if all objects are visible
		isVisible: function() {
			var i, result = true;
			
			if (this.length === 0) {
				result = false;
			} else {
				for (i in this.objects) {
					if (!this.objects[i].isVisible()) {
						result = false;
						break;
					}
				}
			}
			
			return result;
		},
		
		//draws object on canvas
//		draw: function(cx) {
//			this.createStrokePath(cx);
//			cx.stroke();
//			
//			this.createFillPath(cx);
//			cx.fill();
//		},
		
		//returns the most top point of grouped objects
//		top: function() {
//			//TODO: implement CanvasGroup.top()
//			return 0;
//		},
//		
//		//returns the most left point of grouped objects
//		left: function() {
//			//TODO: implement CanvasGroup.left()
//			return 0;
//		},
//		
//		//returns the width from most left to most right point of grouped objects
//		width: function() {
//			//TODO: implement CanvasGroup.width()
//			return 0;
//		},
//		
//		//returns the height from most top to most bottom point of grouped objects
//		height: function() {
//			//TODO: implement CanvasGroup.height()
//			return 0;
//		},
		
		//gets/sets over object
		over: function(obj) {
			if (this.isNull()) {
				return this;
			}
			return this;
			//TODO: think of calling over() of most top grouped object
//			if (obj === undefined) {
//				//getter
//				return this.overObj;
//			} else {
//				//setter
//				
//				//update old neighbours of passed object
//				if (obj.overObj !== null) {
//					obj.overObj.underObj = obj.underObj;
//				}
//				if (obj.underObj !== null) {
//					obj.underObj.overObj = obj.overObj;
//				}
//				
//				//update passed object
//				obj.overObj = this.overObj;
//				obj.underObj = this;
//				
//				////update new neighbours of passed object
//				if (this.overObj !== null) {
//					this.overObj.underObj = obj;
//				}
//				this.overObj = obj;
//			}
		},

		//gets/sets under object
		under: function(obj) {
			if (this.isNull()) {
				return this;
			}
			return this;
			//TODO: think of calling under() of most bottom grouped object
//			if (obj === undefined) {
//				//getter
//				return this.underObj;
//			} else {
//				//setter
//				
//				//update old neighbours of passed object
//				if (obj.overObj !== null) {
//					obj.overObj.underObj = obj.underObj;
//				}
//				if (obj.underObj !== null) {
//					obj.underObj.overObj = obj.overObj;
//				}
//				
//				//update passed object
//				obj.underObj = this.underObj;
//				obj.overObj = this;
//				
//				////update new neighbours of passed object
//				if (this.underObj !== null) {
//					this.underObj.overObj = obj;
//				}
//				this.underObj = obj;
//			}
		},
		
		//returns attribute value of first grouped objects or
		//sets attribute(s) value of all grouped objects
		attr: function(name, value) {
			if (this.isNull()) {
				return this;
			}
			
			var i, result = this;
			
			if (typeof name === "string" && value === undefined) {
				//getting value
				try {
					result = this.get(0).attr(name);
				} catch (e) {
					result = undefined;
				}
			} else {
				//setting value(s)
				try {
					for (i in this.objects) {
						this.objects[i].attr(name, value);
					}
				} catch (e) {}
			}
			
			return result;
		},
		
		//registers object animation
		animate: function(attr, duration) {
			if (this.isNull()) {
				return this;
			}
			
			var i;
			for (i in this.objects) {
				this.objects[i].animate(attr, duration);
			}
			return this;
		},
		
		//registers click event handler or triggers click event
		click: function(fn) {
			if (this.isNull()) {
				return this;
			}
			
			var i;
			for (i in this.objects) {
				this.objects[i].click(fn);
			}
			return this;
		},
		
		//binds event handler
		bind: function(event, fn) {
			if (this.isNull()) {
				return this;
			}
			
			var i;
			for (i in this.objects) {
				this.objects[i].bind(event, fn);
			}
			return this;
		},
		
		//triggers event
		trigger: function(event, data) {
			if (this.isNull()) {
				return this;
			}
			
			var i;
			for (i in this.objects) {
				this.objects[i].trigger(event, data);
			}
			return this;
		}
				
	};
	
	//</editor-fold>
	
	
	//<editor-fold defaultstate="collapsed" desc="### AnimationQueue ###">
	
	var
		//stores info about animation queue, fires animations
		AnimationQueue = function() {
			
		};
	
	AnimationQueue.prototype = {
		
		//stores first animation in queue
		first: null,
		
		//stores last animation in queue
		last: null,
		
		//stores count of animations in queue
		length: 0,
		
		//returns true, if there is no animation in queue
		isEmpty: function() {
			return this.length === 0;
		},
		
		//adds animation to queue
		add: function(animation) {
			if (!this.isEmpty()) {
				this.last.after = animation;
			} else {
				//queue is empty, so added animation is also first
				this.first = animation;
			}
			this.last = animation;
			
			this.length++;
		},
		
		//removes first animation from queue
		remove: function(animation) {
			if (this.isEmpty()) { //if queue is empty, just return null
				return null;
			}
			
			var result = this.first;
			
			if (this.length === 1) {
				this.last = null;
				this.first = null;
			} else {
				this.first = this.first.after;
			}
			
			this.length--;
			
			return result;
		},
		
		//returns first animation in queue
		peek: function() {
			return this.first;
		},
		
		//returns queue as array
		get: function() {
			var anim = this.first,
				result = [ ];
			
			while (anim !== null) {
				result.push(anim);
				anim = anim.after;
			}
			
			return result;
		},
		
		//removes all animations except the first one
		removeWaiting: function() {
			if (this.length > 1) {
				this.last = this.first;
				this.length = 1;
				this.first.after = null;
			}
		},
		
		//removes all animations
		clear: function() {
			this.last = null;
			this.first = null;
			this.length = 0;
		}
		
	};
	
	//</editor-fold>
	
	
	//<editor-fold defaultstate="collapsed" desc="### Animation ###">
	
	var
		//stores info about animation and about its progress
		Animation = function(obj, attr, options) {
			var name;
			
			//check if attributes are animatable
			for (name in attr) {
				if (obj.animatableAttrs.indexOf(name) === -1) {
					throw new Error("Attribute " + name + " is not animatable.");
				}
			}
			
			this.obj = obj;
			this.modification = attr;
			this.duration = Animation.getDuration(options.duration);
			
			if (typeof options.complete === "function") {
				this.complete = options.complete;
			}
			
			if (typeof options.repeat === "number" || typeof options.repeat === "string") {
				this.repeats = +options.repeat;
			}
		};
	
	//returns duration in milliseconds
	Animation.getDuration = function(duration) {
		if (typeof duration === "number") {
			return duration;
		} else if (duration === "slow") {
			return 600;
		} else if (duration === "fast") {
			return 200;
		} else if (typeof duration === "string") {
			return parseFloat(duration, 10);
		} else {
			return 400;
		}
	};
		
	Animation.prototype = {
		
		//stores reference to animated object
		obj: null,
		
		//reference to next animation
		after: null,
		
		//stores
		modification: null,
		
		//stores initial state of attributes at the animation beginning
		startAttrs: null,
				
		//stores desired state of attributes at the animation end
		endAttrs: null,
		
		//stores duration of animation
		duration: -1,
		
		//stores time when animation was started
		started: -1,
		
		//stores time when animation should finish
		ending: -1,
		
		//stores if animation already finished
		finished: false,
		
		//stores function to be executed when animation finishes
		complete: null,
		
		//stores how many times should be animation repeated
		repeats: 1,
		
		//stores total duration of pauses
		pauseDuration: 0,
		
		//stores when current pause was started
		pauseStarted: -1,
		
		//starts animation
		start: function(time) {
			if (time === undefined) {
				time = sCanvas.now();
			}
			
			this.started = time;
			this.ending = this.started + this.duration;
			
			this.computeAttrs();
		},
		
		//sets start and end attribute values
		computeAttrs: function() {
			var name, value, computedValue;
			
			//if attrs are already computed, do not compute them again
			if (this.startAttrs !== null) {
				return;
			}
			
			this.startAttrs = { };
			this.endAttrs = { };
			
			for (name in this.modification) {
				this.startAttrs[name] = this.obj.attr(name);
				
				value = this.modification[name];
				
				if (typeof value === "string") {
					if (value.charAt(1) === "=") {
						if (value.charAt(0) === "+") {
							this.endAttrs[name] = this.startAttrs[name] + parseFloat(value.slice(2), 10);
						} else if (value.charAt(0) === "-") {
							this.endAttrs[name] = this.startAttrs[name] - parseFloat(value.slice(2), 10);
						} else {
							this.endAttrs[name] = this.startAttrs[name];
						}
					} else {
						this.endAttrs[name] = parseFloat(value, 10);
					}
				} else if (typeof value === "number") {
					this.endAttrs[name] = value;
				} else if (typeof value === "function") {
					computedValue = value.apply(this.obj, [ this.startAttrs[name] ]);
					if (typeof computedValue === "number") {
						this.endAttrs[name] = computedValue;
					} else {
						this.endAttrs[name] = this.startAttrs[name];
					}
				} else {
					this.endAttrs[name] = this.startAttrs[name];
				}
			}
		},
		
		//counts object attributes for next animation frame
		next: function(time) {
			var name, result;
			
			//if animation is currently pause, adjust time
			if (this.isPaused()) {
				time = this.pauseStarted;
			}
			
			if (time > this.ending) {
				return this.finish();
			} else {
				result = { };
				for (name in this.endAttrs) {
					result[name] = this.startAttrs[name] + (this.endAttrs[name] - this.startAttrs[name]) * ((time - this.started - this.pauseDuration) / this.duration);
				}
				return result;
			}
		},
		
		//immediatelly finishes animation
		finish: function() {
			this.finished = true;
			return this.endAttrs;
		},
		
		//returns true if animation already finished
		isFinished: function() {
			return this.finished;
		},
		
		//returns true if animations repeats
		isRepeated: function() {
			return this.repeats > 1;
		},
		
		//restarts animation and lowers animation repetition
		repeat: function(stopped) {
			if (this.isRepeated()) {
				this.finished = false;
				this.pauseDuration = 0;
				this.pauseStarted = -1;
				this.start(stopped === true ? undefined : this.ending);
				this.repeats--;
			}
		},
		
		//pauses animation
		pause: function() {
			if (!this.isPaused()) {
				this.pauseStarted = sCanvas.now();
			}
		},
		
		//ends pause
		play: function() {
			var p;
			if (this.isPaused()) {
				p = sCanvas.now() - this.pauseStarted;
				this.pauseDuration += p;
				this.ending += p;
				this.pauseStarted = -1;
			}
		},
		
		//returns true if animation is paused
		isPaused: function() {
			return this.pauseStarted !== -1;
		}
		
	};
	
	//</editor-fold>
	
	
	//make sCanvas visible
	window.sCanvas = sCanvas;
	
}(window));